﻿module game.battle.battlescene;

import std.random;
import std.container;
import std.algorithm;
import std.typetuple;
import std.array;

import gamelib.types;
import gamelib.util;
import gamelib.graphics.renderer2d.renderer;
import gamelib.graphics.renderer2d.textureview;
import gamelib.graphics.renderer2d.font;

import game.world;
import game.controls;

import game.units;
import game.transformmapper;
import game.battle.background;
import game.battle.radar;
import game.battle.player;

import game.battle.enemies.suicide;
import game.battle.enemies.fighter;
import game.battle.enemies.missileboat;
import game.battle.enemies.missile;
import game.battle.enemies.backshooter;

import game.battle.projectiles.projectilescontroller;
import game.battle.projectiles.playerprojectiles;
import game.battle.projectiles.enemyprojectiles;
import game.battle.projectiles.fireprojectiles;

import game.battle.collisionmap;

final class BattleScene
{
private:
    World mWorld;

    Random mRand;

    BaseFont mFont;
    ControlsState mControlsState;

    enum scalar_t RadarRange = 2500;
    enum Point PlayerPos = {x:World.Width / 2,y:World.Height * 2 / 3};
    enum RadarSize = World.Height - PlayerPos.y;
    TransformMapper mMapper;
    Background mBack;
    Radar mRadar;
    Player mPlayer;
    struct Enemies
    {
        Array!Suicide     suicides;
        Array!Fighter     fighters;
        Array!MissileBoat boats;
        Array!BackShooter backShooters;
        Array!Missile     missiles;
    }
    Enemies mEnemies;

    struct Projectiles
    {
        @disable this(this);
        Array!SimplePlayerProjectile  playerProjectiles;
        Array!SimpleEnemyProjectile   enemyProjectiles;
        Array!SimpleFireProjectile    fireProjectiles;
        Array!ExpandingFireProjectile expFireProjectiles;
        Array!SmokeProjectile         smokeProjectiles;
    }
    struct ProjectileRenderers
    {
        @disable this(this);
        SimplePlayerProjectile.ProjectileRenderer  playerProjectiles;
        SimpleEnemyProjectile.ProjectileRenderer   enemyProjectiles;
        SimpleFireProjectile.ProjectileRenderer    fireProjectiles;
        ExpandingFireProjectile.ProjectileRenderer expFireProjectiles;
        SmokeProjectile.ProjectileRenderer         smokeProjectiles;
    }
    Projectiles mProjectiles;
    ProjectileRenderers mProjectileRenderers;
    ProjectilesController mProjectilesController;

    bool mPaused ;
    uint mWave;
    int mNumUpdates;
    pos_t mNearestEnemyPos;

public:
    @property auto world()  inout pure nothrow { return mWorld; }
    @property auto player() inout pure nothrow { return mPlayer; }
    @property ref inout(ControlsState) controlsState() inout pure nothrow { return mControlsState; }

    @property auto textureManager() inout pure nothrow { return world.textureManager; }
    @property auto fontManager()    inout pure nothrow { return world.fontManager; }

    this(World w)
    {
        mWorld = w;
        mFont = world.fontManager.font!"font1";
        mMapper = TransformMapper(Rect(0,0,World.Width,World.Height), PlayerPos);
        mBack   = new Background(Rect(0,0,World.Width,World.Height));
        Rect radarRect = {x: PlayerPos.x - RadarSize / 2, y: PlayerPos.y - RadarSize / 2, w: RadarSize, h: RadarSize};
        mRadar = new Radar(radarRect);
        mPlayer = new Player(this);
        reset(0,0);
    }

    void spawnProjectile(T)(in T projectile)
    {
        mProjectilesController.spawn(mProjectiles, projectile);
    }

    void spawnEntity(T)(in pos_t pos, in pos_t speed, in rot_t dir)
    {
        auto ent = new T(this);
        ent.pos = pos;
        ent.speed = speed;
        ent.dir = dir;
        foreach(ref c; mEnemies.tupleof)
        {
            import std.traits;
            alias PT = Unqual!(typeof(c[0]));
            static if(is(Unqual!T : PT))
            {
                c ~= ent;
                return;
            }
        }
        assert(false);
    }

    void processAction(in Actions ac, bool pressed) pure
    {
        mControlsState.processAction(ac, pressed);
        if(Actions.ESC == ac && pressed)
        {
            mPaused = !mPaused;
        }
    }

    bool update()
    {
        if(mPaused && mControlsState.attack) 
        {
            return false;
        }

        if(!mPaused)
        {
            updateLevel();
            updateEntities();
            checkCollisions();
            if(player.alive)
            {
                ++mNumUpdates;
            }
        }
        
        return true;
    }

    void draw(Renderer ren)
    {
        mMapper.viewPos = mPlayer.pos;
        mMapper.viewDir = mPlayer.dir;

        drawBack(ren);
        drawRadar(ren);
        drawEnemies(ren);
        drawPlayer(ren);
        drawProjectiles(ren);
        drawHUD(ren);
    }

    void reset(uint seed, uint wave)
    {
        mPlayer = new Player(this);
        foreach(ref t; mEnemies.tupleof)
        {
            t.length = 0;
        }
        foreach(ref c; mProjectiles.tupleof)
        {
            c.length = 0;
        }
        import core.memory;
        GC.collect();

        mRand.seed(seed);
        mWave = wave;
        mPaused = false;
        mNumUpdates = 0;
    }

private:
    void drawRadar(Renderer ren)
    {
        ren.drawColor = Color(255,255,0);
        foreach(ref t; mEnemies.tupleof)
        {
            import std.traits;
            static if(is(Unqual!(typeof(t[0])): Missile))
            {
                ren.drawColor = ColorRed;
            }
            mRadar.draw(ren, RadarRange, player.pos, player.dir, t[0..$]);
        }
        
        if(enemyCount > 0)
        {
            auto dir = (mNearestEnemyPos - mPlayer.pos);
            dir = (dir / cast(scalar_t)2).normalized; //prevent overflows
            dir = toRotMat(mPlayer.dir).transposed * dir;

            Point pts[2];
            foreach(i;0..2)
            {
                pts[i].x = PlayerPos.x - cast(int)(dir.y * (32 + i * 16));
                pts[i].y = PlayerPos.y - cast(int)(dir.x * (32 + i * 16));
            }
            ren.drawColor = ColorRed;
            ren.drawLines(pts);
        }
    }

    void drawEnemies(Renderer ren)
    {
        foreach(ref t; mEnemies.tupleof)
        {
            foreach(e;t[0..$])
            {
                e.draw(ren, mMapper);
            }
        }
    }

    void drawBack(Renderer ren)
    {
        mBack.draw(ren,PlayerPos,mPlayer.pos,mPlayer.dir);
    }
    
    void drawPlayer(Renderer ren)
    {
        mPlayer.draw(ren, PlayerPos);
    }
    
    void drawProjectiles(Renderer ren)
    {
        mProjectilesController.draw(mProjectiles, mProjectileRenderers, ren, mMapper);
    }
    
    void drawHUD(Renderer ren)
    {
        Rect screenRc = {x: 0, y: 0, w: World.Width, h: World.Height};
        auto msec = World.UpdateInterval * mNumUpdates;
        auto seconds = (msec / 1000) % 60;
        auto minutes = (msec / 1000) / 60;
        msec %= 1000;
        mFont.outText(ren, screenRc,
                      world.formatStr("Time: %02s:%02s.%03s\nHealth: %s\nKilled: %s\nRemaining: %s\nWave: %s\nCombo: %s",
                      minutes, seconds, msec, player.health, player.killed, enemyCount, mWave, player.currentCombo),
                      BaseFont.HALIGN_LEFT | BaseFont.VALIGN_BOTTOM, ColorGreen);
        if(mPaused)
        {
            mFont.outText(ren, screenRc, "      PAUSED\nPress ATTACK to exit", BaseFont.ALIGN_CENTER, ColorGreen);
        }
    }
    
    void updateLevel()
    {
        if(0 == enemyCount)
        {
            alias      EnemiesTypes   = TypeTuple!(Suicide,Fighter,BackShooter,MissileBoat);
            enum int[] EnemiesWeights =           [10     ,30     ,50         ,200];
            static assert(EnemiesTypes.length == EnemiesWeights.length);
            static assert(isSorted(EnemiesWeights));
            int weigtht = 10 + mWave * 50;
            import gamelib.util;
            foreach_reverse(i;TupleRange!(0, EnemiesTypes.length))
            {
                const maxCount = weigtht / EnemiesWeights[i];
                if(0 == maxCount) continue;
                const count = (1 == maxCount) ? 1 : uniform!"[]"(1,maxCount, mRand);
                foreach(j;0..count)
                {
                    auto pos = pos_t(cast(scalar_t)uniform(cast(int)mPlayer.pos.x-1000,cast(int)mPlayer.pos.x+1000,mRand),
                                     cast(scalar_t)uniform(cast(int)mPlayer.pos.y-1000,cast(int)mPlayer.pos.y+1000,mRand));
                    auto dir = -(mPlayer.pos - pos).normalized;
                    spawnEntity!(EnemiesTypes[i])(pos, ZeroPos, dir);
                }
                weigtht -= EnemiesWeights[i];
            }
            ++mWave;
        }
    }

    void updateEntities()
    {
        auto minDist = scalarw_t.max;
        mProjectilesController.update(mProjectiles);
        foreach(ti;TupleRange!(0,2))
        {
            foreach(ref t; mEnemies.tupleof)
            {
                static if(0 == ti)
                {
                    foreach_reverse(e;t[0..$])
                    {
                        e.updateAi();
                    }
                }
                else static if(1 == ti)
                {
                    auto len = t.length;
                    auto i = len - 1;
                    foreach_reverse(e;t[0..$])
                    {
                        if(!e.process())
                        {
                            --len;
                            t[i] = t[len];
                        }
                        else
                        {
                            auto dist = (mPlayer.pos - e.pos).magnitude_squared;
                            if(dist < minDist)
                            {
                                minDist = dist;
                                mNearestEnemyPos = e.pos;
                            }
                        }
                        --i;
                    }
                    t.length = len;
                }
                else
                {
                    static assert(false);
                }
            }
        }
        
        mPlayer.process();
    }

    void checkCollisions()
    {
        //check projectile collisions
        foreach(ref projCont;mProjectiles.tupleof)
        {
            size_t i = 0;
            foreach(proj;projCont[0..$])
            {
                checkCollisionProj(proj);
                projCont[i] = proj;
                ++i;
            }
        }

        //check entitys collisions with player
        foreach(ref entCont;mEnemies.tupleof)
        {
            foreach(ent;entCont[0..$])
            {
                static if(__traits(compiles,checkCollision(this,mPlayer,ent)))
                {
                    checkCollision(this,mPlayer,ent);
                }
            }
        }
    }
    
    void checkCollisionProj(T)(ref T proj)
    {
        static if(__traits(compiles,checkCollision(this,proj,mPlayer)))
        {
            if(checkCollision(this,proj,mPlayer))
            {
                return;
            }
        }

        foreach(ref entCont;mEnemies.tupleof)
        {
            foreach(ent;entCont[0..$])
            {
                static if(__traits(compiles,checkCollision(this,proj,ent)))
                {
                    if(checkCollision(this,proj,ent))
                    {
                        return;
                    }
                }
            }
        }
    }
    
    @property auto enemyCount() const pure nothrow
    {
        uint ret = 0;
        foreach(ref entCont;mEnemies.tupleof)
        {
            ret += entCont.length;
        }
        return ret;
    }
}

